Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/phoenixframework/phoenix_live_view/llms.txt

Use this file to discover all available pages before exploring further.

This guide covers testing full LiveView modules, including mounting, event handling, navigation, and async operations.

Mounting LiveViews

Basic Mount

Use live/2 to mount a LiveView and get the rendered HTML:
test "mounts and renders", %{conn: conn} do
  {:ok, view, html} = live(conn, "/thermo")
  
  assert html =~ "The temp is: 1"
  assert view.module == MyAppWeb.ThermoLive
end
The live/2 macro returns:
  • {:ok, view, html} - Successfully mounted view and initial HTML
  • {:error, {:redirect, %{to: path}}} - LiveView redirected during mount
  • {:error, {:live_redirect, %{to: path}}} - LiveView live-redirected during mount

Testing Mount with Redirects

Some LiveViews redirect during mount:
test "redirects when unauthorized", %{conn: conn} do
  assert {:error, {:redirect, %{to: "/login"}}} = live(conn, "/admin")
end

Disconnected and Connected Mounts

Test both lifecycle phases:
test "disconnected and connected mount", %{conn: conn} do
  # Disconnected mount (HTTP GET)
  conn = get(conn, "/thermo")
  assert html_response(conn, 200) =~ "The temp is: 0"
  
  # Connected mount (WebSocket)
  {:ok, view, html} = live(conn)
  assert html =~ "The temp is: 1"
end

Mount with Session and Params

Pass session data and connection parameters:
test "mounts with session", %{conn: conn} do
  conn = 
    conn
    |> Plug.Test.init_test_session(%{user_id: 123})
    |> put_connect_params(%{"token" => "abc"})
  
  {:ok, view, html} = live(conn, "/dashboard")
  assert html =~ "User: 123"
end
Use put_connect_params/2 to set parameters available via get_connect_params/1 in your LiveView’s mount callback.

Testing Events

LiveView provides functions to test all phx-* event bindings.

Click Events

Test phx-click events by finding elements:
test "handles click events", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/thermo")
  
  # Click by finding element with text
  assert view
         |> element("button", "Increment")
         |> render_click() =~ "The temp is: 2"
  
  # Click by ID
  assert view
         |> element("#dec-button")
         |> render_click() =~ "The temp is: 1"
end
You can also trigger events directly:
test "triggers click event directly", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/thermo")
  
  assert render_click(view, :inc, %{}) =~ "The temp is: 2"
end
Prefer using element/3 over direct event calls, as it validates that the element and event actually exist in your rendered HTML.

Form Events

Form Change Events

Test phx-change validation:
test "validates form on change", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/users/new")
  
  # Test validation errors
  assert view
         |> form("#user-form", user: %{name: ""})
         |> render_change() =~ "can't be blank"
  
  # Test valid input
  refute view
         |> form("#user-form", user: %{name: "Alice"})
         |> render_change() =~ "can't be blank"
end

Form Submit Events

Test phx-submit form submissions:
test "submits form", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/users/new")
  
  assert view
         |> form("#user-form", user: %{name: "Alice", email: "alice@example.com"})
         |> render_submit() =~ "User created successfully"
end

Hidden Form Fields

For hidden fields not in the form data, pass them to render_submit/2:
test "submits with hidden fields", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/users/new")
  
  assert view
         |> form("#user-form", user: %{name: "Alice"})
         |> render_submit(%{user: %{"hidden_token" => "secret"}}) =~ "Success"
end
Anti-pattern: Don’t pass regular input field values to render_submit/2. Always pass visible fields through form/3 to ensure they exist in your template. Only use the value parameter for hidden fields.

Form Submit Button

Test specific submit buttons with put_submitter/2:
test "submits with specific button", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/posts/1/edit")
  
  # Click "Save Draft" button
  assert view
         |> form("#post-form", post: %{title: "Draft"})
         |> put_submitter("button[name=action][value=draft]")
         |> render_submit() =~ "Draft saved"
  
  # Click "Publish" button
  assert view
         |> form("#post-form", post: %{title: "Published"})
         |> put_submitter("button[name=action][value=publish]")
         |> render_submit() =~ "Post published"
end

Keyboard Events

Test phx-keydown, phx-keyup, and phx-window-keydown events:
test "handles keyboard events", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/thermo")
  
  # Keyup event
  assert view
         |> element("#temp-input")
         |> render_keyup(%{"key" => "ArrowUp"}) =~ "The temp is: 2"
  
  # Keydown event
  assert render_keydown(view, :key, %{"key" => "ArrowDown"}) =~ "The temp is: 1"
end

Focus and Blur Events

Test phx-focus and phx-blur events:
test "handles focus events", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/search")
  
  # Focus input
  assert view
         |> element("#search-input")
         |> render_focus() =~ "Search active"
  
  # Blur input
  assert view
         |> element("#search-input")
         |> render_blur() =~ "Search inactive"
end

Hook Events

Test events from JavaScript hooks with render_hook/3:
test "handles hook events", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/canvas")
  
  assert render_hook(view, :draw, %{x: 10, y: 20}) =~ "Drawing"
end
For hooks targeting components:
test "hook event to component", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/editor")
  
  assert view
         |> element("#editor-component")
         |> render_hook(:save, %{content: "Hello"}) =~ "Saved"
end

Testing Navigation

Live Patch

Test push_patch navigation within the same LiveView:
test "patches to different tab", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/users")
  
  # Trigger patch
  view
  |> element("a", "Settings")
  |> render_click()
  
  # Assert patch happened
  assert_patch view, "/users?tab=settings"
end
Or manually patch:
test "manually patches view", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/users")
  
  assert render_patch(view, "/users?page=2") =~ "Page 2"
end

Live Navigation

Test push_navigate to different LiveViews:
test "navigates to different LiveView", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/posts")
  
  # Click navigate link
  result = view
           |> element("a", "New Post")
           |> render_click()
  
  # Follow the redirect
  assert {:error, {:live_redirect, %{to: "/posts/new"}}} = result
  
  {:ok, new_view, html} = follow_redirect(result, conn)
  assert html =~ "Create Post"
end

Regular Redirects

Test redirect/2 to non-LiveView pages:
test "redirects to external page", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/dashboard")
  
  result = render_click(view, :logout)
  
  assert {:error, {:redirect, %{to: "/login"}}} = result
  
  {:ok, conn} = follow_redirect(result, conn)
  assert html_response(conn, 200) =~ "Login"
end

Testing Messages

LiveViews are GenServers and can receive messages:
test "handles messages", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/thermo")
  
  # Send message to LiveView process
  send(view.pid, {:set_temp, 50})
  
  # Assert the view updated
  assert render(view) =~ "The temp is: 50"
end

Testing Async Operations

Wait for assign_async, start_async, and stream_async operations:
test "loads data asynchronously", %{conn: conn} do
  {:ok, view, html} = live(conn, "/users")
  
  # Initial loading state
  assert html =~ "Loading users..."
  
  # Wait for async operation to complete
  assert render_async(view) =~ "Alice"
  assert render_async(view) =~ "Bob"
end
With custom timeout:
test "waits for slow async operations", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/reports")
  
  # Wait up to 5 seconds
  assert render_async(view, 5000) =~ "Report generated"
end

Testing Uploads

Test file uploads with file_input/4 and render_upload/3:
test "uploads file", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/upload")
  
  # Create file input
  avatar = file_input(view, "#upload-form", :avatar, [
    %{
      last_modified: 1_594_171_879_000,
      name: "avatar.jpg",
      content: File.read!("test/fixtures/avatar.jpg"),
      size: 10_000,
      type: "image/jpeg"
    }
  ])
  
  # Upload file
  assert render_upload(avatar, "avatar.jpg") =~ "100%"
  
  # Submit form
  assert view
         |> form("#upload-form")
         |> render_submit() =~ "File uploaded"
end

Chunked Uploads

Test progressive upload:
test "uploads file in chunks", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/upload")
  
  avatar = file_input(view, "#upload-form", :avatar, [%{...}])
  
  # Upload 50%
  assert render_upload(avatar, "avatar.jpg", 50) =~ "50%"
  
  # Upload remaining 50%
  assert render_upload(avatar, "avatar.jpg", 50) =~ "100%"
end

Element Selection

The element/3 function finds elements by CSS selector:
# By ID
element(view, "#submit-button")

# By class and text
element(view, "button.primary", "Save")

# By attribute
element(view, ~s{[data-user-id="123"]})

# Complex selector
element(view, "#user-list > li:first-child a")

Text Filters

Use text filters to narrow selection:
# Exact text match (substring)
element(view, "button", "Save")

# Regex match
element(view, "a", ~r/^Edit$/)

# Avoid unintended matches
element(view, "a", ~r/(?<!un)opened/)  # Matches "opened" but not "unopened"
Add data-test-id attributes to elements that are hard to select with CSS alone:
<button data-test-id="save-draft">Save Draft</button>
Then select with:
element(view, "[data-test-id='save-draft']")

Checking Element Existence

Use has_element?/1 and has_element?/3:
test "shows and hides elements", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/users/1")
  
  assert has_element?(view, "#user-details")
  
  # Delete user
  view
  |> element("button", "Delete")
  |> render_click()
  
  refute has_element?(view, "#user-details")
end

Rendering Views

The render/1 function returns the current HTML:
test "renders current state", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/counter")
  
  current_html = render(view)
  assert current_html =~ "Count: 0"
end
Render a specific element:
test "renders element content", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/users")
  
  user_list = view
              |> element("#user-list")
              |> render()
  
  assert user_list =~ "Alice"
end

Testing Page Title

Assert on page title updates:
test "updates page title", %{conn: conn} do
  {:ok, view, _html} = live(conn, "/posts/1")
  
  render_click(view, :edit)
  
  assert page_title(view) =~ "Edit Post"
end

Testing Nested LiveViews

Access child LiveViews with live_children/1 and find_live_child/2:
test "interacts with child LiveView", %{conn: conn} do
  {:ok, parent, _html} = live(conn, "/dashboard")
  
  # Get all children
  [clock, weather] = live_children(parent)
  
  # Or find specific child by ID
  clock = find_live_child(parent, "clock")
  
  assert render_click(clock, :snooze) =~ "Snoozing"
end

Best Practices

1
Use Element Selection
2
Prefer element/3 over direct event calls to validate your HTML structure.
3
Test User Workflows
4
Test complete user journeys, not just individual functions.
5
Verify HTML Structure
6
Assert on rendered HTML to ensure correct DOM structure and attributes.
7
Test Error Cases
8
Test validation errors, failed operations, and error states.
9
Use Descriptive Selectors
10
Add data-test-id attributes for complex or dynamic elements.